#!/usr/bin/env python
# -*- coding:utf-8 -*-
import traceback
import subprocess
import time
import os
import signal
import psutil

import addon_manager.addon_utils
from addon_manager.addon_venvwrapper import VenvWrapper
from addon_manager.addon_exception import (
    NoSuchAddOnService,
    AddOnServiceNumConstraint,
    AddOnConfigErrLanguageType,
    RunningAddOnOpException,
    StoppedAddOnOpException,
    BuiltinAddOnOperateFailed,
    InvalidAddOnOperateFailed,
)
from addon_manager.addon_config import AddOnConfig
from addon_manager.addon_logger import AddOnLogger
from addon_manager.addon_status import AddOnStatus
from addon_manager.addon_whitelist import AddOnWhitelist
from addon_manager.addon_info import AddOnState


class ExecutorBase(object):
    """
    addon启动器基础接口类
    """

    def __init__(self, addon_name, port_func=None):
        AddOnLogger().info("start {}.".format(addon_name))
        self.addon_name = addon_name
        self.popen = None
        self.config = AddOnConfig.inst()
        self.exit_code = None
        self.service_path = None

        AddOnLogger().debug("init service {}.".format(self.addon_name))

        addon_stat_inst = AddOnStatus.inst()
        self.addon_info = addon_stat_inst.get_addoninfo_by_name(addon_name)
        self.service_path = self.addon_info.get_service_path()  # flow.json

    def start(self):
        """
        addon启动函数
        """
        AddOnLogger().debug("start service {}.".format(self.addon_name))
        # 调用子类的_handle_start函数，如抛出异常，则不显示start ok的日志
        self._handle_start()
        addon_stat_inst = AddOnStatus.inst()
        addon_stat_inst.change_addonstate(self.addon_name, AddOnState.RUNNING)
        AddOnLogger().debug("start service {} ok.".format(self.addon_name))

    def _check_before_start(self):
        if self.is_alive():
            raise RunningAddOnOpException()

        self._check_if_service_path_exists()

    def _handle_start(self):
        raise NotImplementedError()

    def stop(self):
        """
        addon停止函数
        """
        AddOnLogger().debug("stop service {}.".format(self.addon_name))
        # 调用子类的_handle_stop函数，如抛出异常，则不显示stop ok的日志
        self._handle_stop()
        addon_stat_inst = AddOnStatus.inst()
        if self.addon_name in addon_stat_inst:
            addon_stat_inst.change_addonstate(self.addon_name, AddOnState.STOPPED)
        AddOnLogger().debug("stop service {} ok.".format(self.addon_name))

    def _check_before_stop(self):
        if not self.is_alive():
            raise StoppedAddOnOpException()

    def _handle_stop(self):
        raise NotImplementedError()

    def _check_file_folder_exists(self, file_path):
        if file_path:
            if not os.path.exists(os.path.dirname(file_path)):
                os.makedirs(os.path.dirname(file_path))

    def _start_subprocess(self, execute_args, **args):
        AddOnLogger().debug("subprocess popen: %s", execute_args)

        stderr = args.get("stderr")
        stdout = args.get("stdout")
        env = os.environ
        if args.get("env") != None:
            env.update(args.get("env"))

        try:
            if stderr is None:
                self.stderr_path = "/usr/etc/jkzuc/configs/JAKA/AddOns/{}/log/AddOnerrlog.log".format(self.addon_name)
                self._check_file_folder_exists(self.stderr_path)
                stderr = open(self.stderr_path, "a")
                stderr.write("********Program started at {}********\n".format(time.strftime("%Y-%m-%d %H:%M:%S")))

            if stdout is None:
                self.stdout_path = "/usr/etc/jkzuc/configs/JAKA/AddOns/{}/log/AddOnlog.log".format(self.addon_name)
                self._check_file_folder_exists(self.stdout_path)
                stdout = open(self.stdout_path, "a")
                stdout.write("********Program started at {}\n********".format(time.strftime("%Y-%m-%d %H:%M:%S")))

            popen = subprocess.Popen(execute_args, stderr=stderr, stdout=stdout, env=env, cwd=self.addon_info.get_basedir(), close_fds=True)

        except OSError as err_message:
            AddOnLogger().error(traceback.format_exc())
            AddOnLogger().error("start a service failed: OSError " + str(err_message))
            AddOnLogger().error("[AddOnExec]:ERR start a service failed: OSError:{} ".format(err_message))
            raise err_message
        except KeyError as err_message:
            AddOnLogger().error(traceback.format_exc())
            AddOnLogger().error("start a service failed: KeyError  " + str(err_message))
            AddOnLogger().error("[AddOnExec]:ERR start a service failed: KeyError:{} ".format(err_message))
            raise err_message
        except Exception as err_message:
            AddOnLogger().error(traceback.format_exc())
            AddOnLogger().error("start a service failed: Exception " + str(err_message))
            AddOnLogger().error("[AddOnExec]:ERR start a service failed: Exception:{} ".format(err_message))
            raise err_message
        else:
            if popen.pid == 0 or popen.returncode is not None:
                AddOnLogger().debug("Start process [%s] failed." % self.addon_name)
                self.popen = None
            else:
                self.popen = popen
                AddOnLogger().debug(
                    time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + "  Start process  [%s] ,pid [%d] ok" % (self.addon_name, self.popen.pid)
                )

    def _kill_recurse(self, process):
        children = process.children()
        for child in children:
            self._kill_recurse(child)
        AddOnLogger().debug("kill process {} <>".format(process))
        try:
            process.kill()
        except Exception as e:
            AddOnLogger().error("no such process: {} <>".format(process))

    def _stop_subprocess(self):
        try:

            # alive_status = self.get_alive_status_with_servicepath()
            # print('alive_status', alive_status)
            # if not alive_status.has_key(service_path):
            #     AddOnLogger().warn('service %s doesn\'t exist', service_path)
            #     return False

            # if alive_status[service_path]:
            pid = self.popen.pid
            process = psutil.Process(pid)
            self._kill_recurse(process)
            # self.popen.kill()
            # os.killpg(self.process_popen[service_path].pid,sig)
            # self.process_popen[name].send_signal(sig)

            # alive_status = self.get_alive_status_with_servicepath()
            # if alive_status.has_key(service_path) and alive_status[service_path]:
            if self.is_alive():
                AddOnLogger().debug("Waiting for [%s][%s] exit." % (self.addon_name, self.popen.pid))
                # 防止僵尸进程产生
                exit_code = self.popen.wait()
                AddOnLogger().info(
                    time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + "  Process [%s] stoped,exit_code [%d]" % (self.addon_name, exit_code)
                )

            self.popen = None
            AddOnLogger().info(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + "  Stop process  [%s] ,pid [%d] " % (self.addon_name, pid))

        except Exception as e:
            AddOnLogger().error(traceback.format_exc())
            raise e
            # todo 发生异常清理资源

    def is_alive(self):
        """
        addon判活函数
        """
        if self.popen is None:
            return False

        pstat = self.popen.poll()
        if pstat is None:
            self.exit_code = None
            return True
        else:
            self.exit_code = pstat
            return False

    def _check_if_service_path_exists(self):
        if self.service_path is None:
            raise NoSuchAddOnService()

        if not os.path.exists(self.service_path):
            raise NoSuchAddOnService()

    def __del__(self):
        addon_stat_inst = AddOnStatus.inst()
        if self.addon_name in addon_stat_inst:
            addon_stat_inst.change_addonstate(self.addon_name, AddOnState.STOPPED)


class ExecutorPython2(ExecutorBase):
    """
    python2 addon启动器
    """

    def __init__(self, addon_name):
        ExecutorBase.__init__(self, addon_name)

    def _handle_start(self):
        self._check_before_start()

        executor_args = []

        include_paths = ["/usr/lib/python2.7/dist-packages/"]  # for jkzuc
        venvWrapper = VenvWrapper(os.path.dirname(self.service_path))
        if venvWrapper.CheckIfExistVenv():
            executor_args.append(venvWrapper.GetPython())
            venvWrapper.SetModuleSearchPaths(include_paths)
        else:
            executor_args.append("/usr/bin/python2")

        executor_args.append("-u")
        executor_args.append(self.service_path)

        return ExecutorBase._start_subprocess(self, executor_args)

    def _handle_stop(self):
        self._check_before_stop()
        return ExecutorBase._stop_subprocess(self)


class ExecutorNodeRed(ExecutorBase):

    def __init__(self, addon_name):
        ExecutorBase.__init__(self, addon_name)

    def _handle_start(self):
        self._check_before_start()

        port = self.addon_info.get_port()
        basedir = self.addon_info.get_basedir()
        execute_args = ["/usr/local/node/bin/node-red"]
        execute_args += ["-p", str(port)]
        # 指定模块安装目录
        execute_args += ["--userDir", basedir]
        execute_args += [self.service_path]

        new_path = "/usr/local/node/bin/:" + os.environ["PATH"]

        return ExecutorBase._start_subprocess(self, execute_args, env={"PATH": new_path})

    def _handle_stop(self):
        self._check_before_stop()
        return ExecutorBase._stop_subprocess(self)


class ExecutorNode(ExecutorBase):

    def __init__(self, addon_name):
        ExecutorBase.__init__(self, addon_name)

    def _handle_start(self):
        self._check_before_start()

        port = self.addon_info.get_port()
        execute_args = ["/usr/local/node/bin/node"]
        execute_args += [self.service_path]
        # 指定addon服务端口号
        execute_args += ["--port", str(port)]

        return ExecutorBase._start_subprocess(self, execute_args)

    def _handle_stop(self):
        self._check_before_stop()
        return ExecutorBase._stop_subprocess(self)


class ExecutorWrapper(object):
    __EXECUTOR_CLASS_MAP = {
        "python": ExecutorPython2,
        "python2": ExecutorPython2,
        "node-red": ExecutorNodeRed,
        "node": ExecutorNode,
    }

    def __init__(self, config):
        self.config = AddOnConfig.inst()

        self.executors = {}
        self.__try_start_exist_addon()

    def __try_start_exist_addon(self):
        addon_status = AddOnStatus.inst()
        addonlist = addon_status.filter_addon(lambda addoninfo: addoninfo.is_service_enable())
        AddOnLogger().debug("__try_start_exist_addon: %s", addonlist)
        for addoninfo in addonlist:
            try:
                service_path = addoninfo.get_service_path()
                self.start(service_path)
            except Exception:
                AddOnLogger().error(traceback.format_exc())

    def __check_if_alivenum_reach_maxlimit(self):
        if len(self.executors) > self.config.addon_max_limit:
            raise AddOnServiceNumConstraint()

    def start(self, service_path):
        """
        start a new process to execute an  addon service
        @return:  sucessful  is trrue
        @rtype: Bool
        """
        service_path = os.path.abspath(service_path)
        addon_name = addon_manager.addon_utils.get_addonname_from_servicepath(service_path)

        self.__check_if_alivenum_reach_maxlimit()

        executor = self.executors.get(addon_name)

        if executor is None:
            addon_status = AddOnStatus.inst()
            addon_info = addon_status.get_addoninfo_by_name(addon_name)
            if addon_info is None:
                raise NoSuchAddOnService()
            if addon_info.state == AddOnState.EXCEPTION:
                # 异常AddOn不可被启动
                raise InvalidAddOnOperateFailed(addon_name, "start")
            language_type = addon_info.get_languagetype()
            executor_class = self.__EXECUTOR_CLASS_MAP.get(language_type)
            if executor_class is None:
                raise AddOnConfigErrLanguageType(language_type)
            executor = executor_class(addon_name)
        executor.start()
        self.executors[addon_name] = executor

    def _stop_by_addon_name(self, executors, addon_name):
        executor = executors.get(addon_name)
        if executor is None:
            raise NoSuchAddOnService()

        try:
            executor.stop()
        except StoppedAddOnOpException:
            del executors[addon_name]
        else:
            del executors[addon_name]

    # todo  SIGKILL  or SIGTERM /SIGINT
    def stop(self, service_path, sig=signal.SIGINT):
        """
        stop the process
        """
        service_path = os.path.abspath(service_path)
        addon_name = addon_manager.addon_utils.get_addonname_from_servicepath(service_path)

        addon_whitelist = AddOnWhitelist.inst()
        if addon_whitelist.is_builtin_addon(addon_name):
            """
            内置AddOn暂不支持手动停止
            """
            raise BuiltinAddOnOperateFailed(addon_name, "stopped")
        self._stop_by_addon_name(self.executors, addon_name)

    def stop_all(self):
        AddOnLogger().debug("stop all the addon service")

        addon_names = self.executors.keys()
        for addon_name in addon_names:
            try:
                self._stop_by_addon_name(self.executors, addon_name)
            except Exception:
                AddOnLogger().error("stop {} failed: {}".format(addon_name, traceback.format_exc()))

    def get_alive_status(self):
        # _, alive_status = self.get_alive_num_and_status()
        status_dict = {}
        # for key in alive_status:
        #     new_key = key.replace("/usr/etc/jkzuc/configs/JAKA/AddOns/", '').split('/')[0]
        #     status_dict[new_key] = alive_status[key]
        addon_stat_inst = AddOnStatus.inst()
        addon_whitelist = AddOnWhitelist()
        for addon_name in addon_stat_inst.keys():
            if not addon_whitelist.is_display_disable(addon_name):
                executor = self.executors.get(addon_name)
                if executor is None:
                    status_dict[addon_name] = False
                elif executor.is_alive():
                    status_dict[addon_name] = True
                else:
                    status_dict[addon_name] = False
        return status_dict

    def update_status(self):
        """
        更新AddOn端口状态
        """
        addon_names = self.executors.keys()
        addon_stat_inst = AddOnStatus.inst()
        for addon_name in addon_names:
            addon_executor = self.executors[addon_name]
            if not addon_name in addon_stat_inst:
                try:
                    addon_executor.stop()
                    del self.executors[addon_name]
                except StoppedAddOnOpException:
                    del self.executors[addon_name]
                continue

            try:
                if not addon_executor.is_alive():
                    del self.executors[addon_name]
            except NoSuchAddOnService:
                del self.executors[addon_name]


if __name__ == "__main__":
    executor = ExecutorWrapper(AddOnConfig.inst())

    def Test_GetAddOnList():
        addonlist = executor.get_addon_list()
        print(addonlist)

    # Test_GetAddOnList()

    def Test_ProtoBuf():
        # from google.protobuf import json_format
        # import jaka_pb2
        # gaddonList = jaka_pb2.AddOnList()
        # addonlist = executor.get_addon_list()
        # print('[Test_ProtoBuf] {}'.format(addonlist))
        # json_format.Parse(json.dumps({'addOnInfoList': addonlist}), gaddonList)
        # print('[Test_ProtoBuf] protobuf {}'.format(gaddonList))
        pass

    # Test_ProtoBuf()

    def Test_GetAliveStatus():
        alive_status = executor.get_alive_status()
        print(alive_status)

    Test_GetAliveStatus()
